package in.co.codedoc.sql;

import in.co.codedoc.ioc.IOCContainer;

import java.sql.Connection;
import java.util.Stack;

public class DBInteractions
{
	public static void StartSession()
	{
		Stack<DBContext> dbContextStack = GetDBContextStack();
		
		if(dbContextStack.isEmpty())
		{
			if(Logger.logger.isDebugEnabled())
			{
				Logger.logger.debug("StartSession: Staring a new session");
			}
			dbContextStack.push(new DBContext(MakeConnection()));
		}
		else
		{
			if(Logger.logger.isDebugEnabled())
			{
				Logger.logger.debug
				(
					"StartSession: adding ref to existing session (" + 
					dbContextStack.peek().GetSessionRefs() +
					" refs already)"
				);
			}
			dbContextStack.peek().AddSessionRef();
		}
	}
	
	public static void StartNewSession()
	{
		Stack<DBContext> dbContextStack = GetDBContextStack();
		
		if(Logger.logger.isDebugEnabled())
		{
			Logger.logger.debug("StartNewSession: Staring a new session");
		}
		
		dbContextStack.push(new DBContext(MakeConnection()));
	}
	
	public static void EndSession()
	{
		Stack<DBContext> dbContextStack = GetDBContextStack();
		
		if(dbContextStack.peek().HasSessionRefs())
		{
			dbContextStack.peek().ReleaseSessionRef();
			if(Logger.logger.isDebugEnabled())
			{
				Logger.logger.debug
				(
					"EndSession: Releasing session ref (" +
					dbContextStack.peek().GetSessionRefs() +
					" refs now)"
				);
			}
		}
		else
		{
			DBContext dbContext = dbContextStack.pop();
			
			if(Logger.logger.isDebugEnabled())
			{
				Logger.logger.debug("EndSession: Closing Session");
			}
			
			try
			{
				dbContext.GetConnection().close();
			}
			catch(Throwable th)
			{
				throw new RuntimeException(th);
			}
		}
	}
	
	public static void StartTransaction()
	{
		StartSession();
		
		Stack<DBContext> dbContextStack = GetDBContextStack();

		if(Logger.logger.isDebugEnabled())
		{
			Logger.logger.debug
			(
				"StartTransaction. (" + 
				dbContextStack.peek().GetTransactionRefs() +
				" refs already)"
			);
		}
		
		dbContextStack.peek().AddTransactionRef();
	}
	
	public static void StartNewTransaction()
	{
		StartNewSession();
		
		Stack<DBContext> dbContextStack = GetDBContextStack();
		
		if(Logger.logger.isDebugEnabled())
		{
			Logger.logger.debug
			(
				"StartNewTransaction. (" + 
				dbContextStack.peek().GetTransactionRefs() +
				" refs already -- should be ZERO)"
			);
		}
		
		dbContextStack.peek().AddTransactionRef();
	}
	
	public static void CommitTransaction()
	{
		CommitOrRollback(true);
	}
	
	public static void RollbackTransaction()
	{
		CommitOrRollback(false);
	}
	
	public static void CommitOrRollback()
	{
		Stack<DBContext> dbContextStack = GetDBContextStack();
		CommitOrRollback(!dbContextStack.peek().IsRollbackSet());
	}
	
	private static void CommitOrRollback(boolean isCommit)
	{
		try
		{
			Stack<DBContext> dbContextStack = GetDBContextStack();
		
			dbContextStack.peek().ReleaseTransactionRef();
			
			if(Logger.logger.isDebugEnabled())
			{
				Logger.logger.debug
				(
					"CommitOrRollback(" + isCommit 
					+ "): relased a ref. Now has " +
					dbContextStack.peek().GetTransactionRefs() +
					" refs."
				);
			}
			
			if(!dbContextStack.peek().HasTransactionRefs())
			{
				Connection connection = dbContextStack.peek().GetConnection();
				
				isCommit = isCommit && !dbContextStack.peek().IsRollbackSet();
				
				if(isCommit)
				{
					if(Logger.logger.isDebugEnabled())
					{
						Logger.logger.debug
						(
							"CommitOrRollback: COMMIT"
						);
					}
					
					try
					{
						connection.commit();
					}
					catch (Throwable th)
					{
						throw new RuntimeException(th);
					}
				}
				else
				{
					if(Logger.logger.isDebugEnabled())
					{
						Logger.logger.debug
						(
							"CommitOrRollback: ROLLBACK"
						);
					}
					
					try
					{
						connection.rollback();
					}
					catch (Throwable th)
					{
						throw new RuntimeException(th);
					}
					
					dbContextStack.peek().ResetRollback();
				}
			}
			else if(!isCommit)
			{
				dbContextStack.peek().SetRollback();
			}
		}
		finally
		{
			EndSession();
		}
	}

	public static Connection GetConnection()
	{
		Stack<DBContext> dbContextStack = GetDBContextStack();
	
		return dbContextStack.peek().GetConnection();
	}

	private static Connection MakeConnection()
	{
		return IOCContainer.Lookup(DBConnectionProvider.class).GetConnection();
	}
	
	public static boolean IsConfigured()
	{
		return IOCContainer.Lookup(DBConnectionProvider.class) != null;
	}

	private static Stack<DBContext> GetDBContextStack()
	{
		Stack<DBContext> dbContextStack = dbContextStackHolder.get();
		
		if(dbContextStack == null)
		{
			dbContextStackHolder.set(dbContextStack = new Stack<DBContext>());
		}
		
		return dbContextStack;
	}
	
	private static ThreadLocal<Stack<DBContext>> dbContextStackHolder = new ThreadLocal<Stack<DBContext>>();//TODO Make into SessionLocal
	
	private static class DBContext
	{
		public DBContext(Connection connection)
		{
			rollback = false;
			sessionDepth = 0;
			transactionDepth = 0;
			this.connection = connection;
		}
		
		public void ResetRollback()
		{
			rollback = false;
		}

		public short GetSessionRefs()
		{
			return sessionDepth;
		}
		
		public void AddSessionRef()
		{
			sessionDepth++;
		}
		
		public void ReleaseSessionRef()
		{
			sessionDepth--;
		}
		
		public short GetTransactionRefs()
		{
			return transactionDepth;
		}
		
		public void AddTransactionRef()
		{
			transactionDepth++;
		}
		
		public void ReleaseTransactionRef()
		{
			transactionDepth--;
		}
		
		public void SetRollback()
		{
			rollback = true;
		}
		
		public boolean IsRollbackSet()
		{
			return rollback;
		}
		
		public Connection GetConnection()
		{
			return connection;
		}
		
		public boolean HasTransactionRefs()
		{
			return transactionDepth > 0;
		}
		
		public boolean HasSessionRefs()
		{
			return sessionDepth > 0;
		}
		
		private Connection connection;
		private boolean rollback;
		private short sessionDepth;
		private short transactionDepth;
	}
	
	static
	{
		try
		{
			Class.forName("com.mysql.jdbc.Driver");
		}
		catch (ClassNotFoundException e)
		{
			throw new RuntimeException(e);
		}
	}
}
